package program

import (
	"io/ioutil"
	"mi/compiler/ast"
	"mi/compiler/binder"
	"mi/compiler/builder"
	"mi/compiler/checker"
	"mi/compiler/diagnostic"
	"mi/compiler/emitter"
	"mi/compiler/host"
	"mi/compiler/performance"
	"mi/internal/system"
	"mi/internal/xbase"
	"mi/internal/xpath"
	"os"
	"path"
	"path/filepath"
	"strings"
)

type Program interface {
	GetProjectFile(uri xpath.URI) *ast.ProjectFile
	GetProjectSourceFiles(pro *ast.ProjectFile) []*ast.SourceFile
	GetSourceFile(uri xpath.URI) *ast.SourceFile
	GetPackageTree(path string) *ast.PackageTree
	GetPackage(pro *ast.ProjectFile, tree *ast.PackageTree) *ast.Package
	GetPackageInDefaultLibrary(tree *ast.PackageTree) *ast.Package
	GetPackagePath(pkg *ast.Package) string
	GetPackages() []*ast.Package
	GetProjectFiles() []*ast.ProjectFile
	GetSourceFiles() []*ast.SourceFile
	GetCompilerHost() host.CompilerHost

	Emit() emitter.EmitResult
	Build(run bool) builder.BuildResult

	GetOptionsDiagnostics() []*diagnostic.Diagnostic
	GetGlobalDiagnostics() []*diagnostic.Diagnostic
	GetSourceFileSyntacticDiagnostics(sourceFile *ast.SourceFile) []*diagnostic.Diagnostic
	GetSourceFileSemanticDiagnostics(sourceFile *ast.SourceFile) []*diagnostic.Diagnostic
	GetCompileDiagnostics() []*diagnostic.Diagnostic

	GetTypeChecker() checker.TypeChecker
	GetEmitResolver() checker.EmitResolver

	// For testing purposes only.  Should not be used by any other consumers (including the language service).
	GetDiagnosticsProducingTypeChecker() checker.TypeChecker
	DropDiagnosticsProducingTypeChecker()

	GetNodeCount() int
	GetIdentifierCount() int
	GetSymbolCount() int
	GetTypeCount() int
}

type program struct {
	rootNames      []string
	host           host.CompilerHost
	notFoundMIROOT bool

	projectFiles       []*ast.ProjectFile
	projectFilesByName map[string]*ast.ProjectFile
	loadProjectStack   []*ast.ProjectFile
	errors             []*diagnostic.Diagnostic
	packageRoot        *ast.PackageTree
	targetProjects     []xpath.URI

	diagnosticsProducingTypeChecker checker.TypeChecker
	noDiagnosticsTypeChecker        checker.TypeChecker
	classifiableNames               map[string]string

	fileProcessingDiagnostics diagnostic.Collection
	programDiagnostics        diagnostic.Collection
}

func CreateProgram(projectFiles []xpath.URI, host host.CompilerHost) Program {
	var p = new(program)
	p.targetProjects = projectFiles
	p.host = host
	p.projectFilesByName = make(map[string]*ast.ProjectFile)
	p.packageRoot = &ast.PackageTree{
		SubTrees: make(map[string]*ast.PackageTree),
	}

	p.fileProcessingDiagnostics = diagnostic.CreateDiagnosticCollection()
	p.programDiagnostics = diagnostic.CreateDiagnosticCollection()

	var miRoot = os.Getenv("MIROOT")
	p.notFoundMIROOT = len(miRoot) == 0
	if p.notFoundMIROOT {
		p.errors = append(p.errors, diagnostic.CreateCompilerDiagnostic(diagnostic.M_MIROOT_not_set))
		return p
	}

	// processor load default library
	var defaultLibPath = filepath.Join(miRoot, "/标准库/项目.mip")
	p.processProjectFile(xpath.URIFromPath(defaultLibPath), true)

	// processor projectFiles
	for _, file := range projectFiles {
		p.processProjectFile(file, false)
	}

	diagnostic.SortDiagnostic(p.errors)
	return p
}

func containProjectFile(projectFiles []*ast.ProjectFile, project *ast.ProjectFile) bool {
	for _, proFile := range projectFiles {
		if proFile == project {
			return true
		}
	}
	return false
}

func (p *program) GetPackage(pro *ast.ProjectFile, tree *ast.PackageTree) *ast.Package {
	if tree == nil || pro == nil {
		return nil
	}

	if pkg, has := pro.ResolvedPackageTree[tree]; has {
		return p.getResolvedPackage(pkg)
	}

	// First find in default package
	if pkg := p.GetPackageInDefaultLibrary(tree); pkg != nil {
		return p.getResolvedPackage(pkg)
	}

	var loadStack []*ast.ProjectFile
	var resolved = make(map[*ast.ProjectFile]int)
	var find func(deep int, subPro *ast.ProjectFile)
	find = func(deep int, subPro *ast.ProjectFile) {
		if containProjectFile(loadStack, subPro) {
			return
		}

		// Push to load stack
		loadStack = append(loadStack, subPro)
		if subPro == pro {
			resolved[subPro] = deep
			return
		}

		for _, dependent := range subPro.ResolvedDependents {
			find(deep + 1, dependent)
		}
		// Pop load stack
		loadStack = loadStack[:len(loadStack)-1]
	}


	for _, pkg := range tree.Packages {
		find(0, pkg.ProjectFile)
	}

	// Find recent project
	var recentPro *ast.ProjectFile
	for file, deep := range resolved {
		if recentPro == nil {
			recentPro = file
		} else if deep < resolved[recentPro] {
			recentPro = file
		}
	}

	// Find recent package of recent project
	var recentPackage *ast.Package
	for _, pkg := range tree.Packages {
		if pkg.ProjectFile == recentPro {
			recentPackage = pkg
		}
	}

	pro.ResolvedPackageTree[tree] = recentPackage
	return p.getResolvedPackage(recentPackage)
}

func (p *program) GetPackageInDefaultLibrary(tree *ast.PackageTree) *ast.Package {
	for _, pkg := range tree.Packages {
		if pkg.ProjectFile.IsDefault {
			return p.getResolvedPackage(pkg)
		}
	}
	return nil
}

func (p *program) GetPackageTree(path string) *ast.PackageTree {
	var names = strings.Split(path, ".")
	var cur = p.packageRoot
	for _, name := range names {
		cur = cur.SubTrees[name]
		if cur == nil {
			break
		}
	}

	return cur
}

func (p *program) GetPackages() []*ast.Package {
	return p.resolveAllPackages(p.packageRoot)
}

func (p *program) GetPackagePath(pkg *ast.Package) string {
	if pkg.ResolvedPath == nil {
		var cur = pkg.Tree
		var result string
		for cur != nil {
			result = cur.Name + "." + result
			cur = cur.Parent
		}
		pkg.ResolvedPath = xbase.NewString(strings.Trim(result, "."))
	}
	return *pkg.ResolvedPath
}

func (p *program) resolveAllPackages(cur *ast.PackageTree) []*ast.Package {
	var result []*ast.Package
	result = append(result, cur.Packages...)
	for _, tree := range cur.SubTrees {
		result = append(result, p.resolveAllPackages(tree)...)
	}
	return result
}

func (p *program) GetProjectFile(uri xpath.URI) *ast.ProjectFile {
	return p.projectFilesByName[uri.Filename()]
}

func (p *program) GetProjectSourceFiles(pro *ast.ProjectFile) []*ast.SourceFile {
	var result []*ast.SourceFile
	for _, pkg := range p.GetPackages() {
		if pkg.ProjectFile == pro {
			for _, file := range pkg.Files {
				result = append(result, file)
			}
		}
	}
	return result
}

func (p *program) GetProjectFiles() []*ast.ProjectFile {
	return p.projectFiles
}

// Quickly find files through the directory, if the
// package is not loaded, it will be dynamically loaded
func (p *program) GetSourceFile(uri xpath.URI) *ast.SourceFile {
	var fileName = uri.Filename()
	var dir = filepath.Dir(fileName)
	for _, file := range p.projectFiles {
		if pkg, ok := file.Packages[dir]; ok {
			pkg = p.getResolvedPackage(pkg)
			if file, ok := pkg.Files[fileName]; ok {
				return file
			}
		}
	}
	return nil
}

// Return the currently parsed file
func (p *program) GetSourceFiles() []*ast.SourceFile {
	var result []*ast.SourceFile
	for _, pkg := range p.GetPackages() {
		for _, file := range pkg.Files {
			result = append(result, file)
		}
	}
	return result
}

func (p *program) getDiagnosticsHelper(sourceFile *ast.SourceFile, getDiagnostics func(sourceFile *ast.SourceFile) []*diagnostic.Diagnostic) []*diagnostic.Diagnostic {
	return diagnostic.SortAndDeduplicateDiagnostics(getDiagnostics(sourceFile))
}

func (p *program) GetSourceFileSyntacticDiagnostics(sourceFile *ast.SourceFile) []*diagnostic.Diagnostic {
	if sourceFile == nil {
		panic("source file can not be nil in 'GetSourceFileSyntacticDiagnostics'")
	}
	return p.getDiagnosticsHelper(sourceFile, p.getSyntacticDiagnosticsForFile)
}

func (p *program) GetSourceFileSemanticDiagnostics(sourceFile *ast.SourceFile) []*diagnostic.Diagnostic {
	if sourceFile == nil {
		panic("source file can not be nil in 'GetSourceFileSemanticDiagnostics'")
	}
	return p.getDiagnosticsHelper(sourceFile, p.getSemanticDiagnosticsForFile)
}

func (p *program) getSyntacticDiagnosticsForFile(sourceFile *ast.SourceFile) []*diagnostic.Diagnostic {
	return diagnostic.GetSourceFileDiagnostics(sourceFile, diagnostic.ParseDiagnostics)
}

func (p *program) getSemanticDiagnosticsForFile(sourceFile *ast.SourceFile) []*diagnostic.Diagnostic {
	var typeChecker = p.GetDiagnosticsProducingTypeChecker()

	var bindDiagnostics = diagnostic.GetSourceFileDiagnostics(sourceFile, diagnostic.BindDiagnostics)
	var checkDiagnostics = typeChecker.GetSingleFileDiagnostics(sourceFile)
	var fileProcessingDiagnosticsInFile = p.fileProcessingDiagnostics.GetDiagnostics(sourceFile.Uri)
	var programDiagnosticsInFile = p.programDiagnostics.GetDiagnostics(sourceFile.Uri)

	var result []*diagnostic.Diagnostic
	result = append(result, bindDiagnostics...)
	result = append(result, checkDiagnostics...)
	result = append(result, fileProcessingDiagnosticsInFile...)
	result = append(result, programDiagnosticsInFile...)
	return result
}

func (p *program) GetCompilerHost() host.CompilerHost {
	return p.host
}

func (p *program) GetCompileDiagnostics() []*diagnostic.Diagnostic {
	if len(p.targetProjects) == 0 {
		return nil
	}

	var result []*diagnostic.Diagnostic
	var sourceFiles []*ast.SourceFile
	var pro = p.GetProjectFile(p.targetProjects[0])
	for _, pkg := range pro.Packages {
		var loadedPkg = p.getResolvedPackage(pkg)
		for _, sourceFile := range loadedPkg.Files {
			var parseDiagnostics = diagnostic.GetSourceFileDiagnostics(sourceFile, diagnostic.ParseDiagnostics)
			var bindDiagnostics = diagnostic.GetSourceFileDiagnostics(sourceFile, diagnostic.BindDiagnostics)
			result = append(result, parseDiagnostics...)
			result = append(result, bindDiagnostics...)

			sourceFiles = append(sourceFiles, sourceFile)
		}
	}

	var typeChecker = p.GetDiagnosticsProducingTypeChecker()
	result = append(result, typeChecker.GetDiagnostics(sourceFiles)...)
	return result
}

func (p *program) GetOptionsDiagnostics() []*diagnostic.Diagnostic {
	var allDiagnostics []*diagnostic.Diagnostic
	allDiagnostics = append(allDiagnostics, p.fileProcessingDiagnostics.GetGlobalDiagnostics()...)
	allDiagnostics = append(allDiagnostics, p.programDiagnostics.GetGlobalDiagnostics()...)
	return diagnostic.SortAndDeduplicateDiagnostics(allDiagnostics)
}

func (p *program) GetGlobalDiagnostics() []*diagnostic.Diagnostic {
	return diagnostic.FilterDiagnostic(p.errors, func(e *diagnostic.Diagnostic) bool {
		return e.File == nil
	})
}

//func (p *program) fullLoadProject(project *ast.ProjectFile) {
//	for _, pkg := range project.Packages {
//		p.getResolvedPackage(pkg)
//	}
//}

// Load all the packages of the project, but will not parse the source file
func (p *program) processProjectFile(uri xpath.URI, IsDefault bool) *ast.ProjectFile {
	var projectFile *ast.ProjectFile
	if p.projectFilesByName[uri.Filename()] != nil {
		// We've already looked for this project, use cached result
		projectFile = p.projectFilesByName[uri.Filename()]
	} else {
		// We haven't looked for this project, do so now and cache result
		p.projectFilesByName[uri.Filename()] = p.host.GetProjectFile(uri, func(hostErrorMessage string) {
			p.errors = append(p.errors, diagnostic.CreateCompilerDiagnostic(diagnostic.M_Cannot_read_file_0_Colon_1, uri.Filename(), hostErrorMessage))
		})
		projectFile = p.projectFilesByName[uri.Filename()]
		if projectFile != nil {
			projectFile.IsDefault = IsDefault
			p.loadProjectFile(projectFile)

			p.projectFiles = append(p.projectFiles, projectFile)
			var errors = diagnostic.GetSourceFileDiagnostics(projectFile, diagnostic.SyntacticErrors)
			for _, e := range errors {
				p.errors = append(p.errors, e)
			}
		}
	}
	return projectFile
}

func (p *program) makePackagePath(path string) *ast.PackageTree {
	var names = strings.Split(path, ".")
	var cur = p.packageRoot
	for _, name := range names {
		var tree = cur.SubTrees[name]
		if tree == nil {
			tree = &ast.PackageTree{
				Name:     name,
				SubTrees: make(map[string]*ast.PackageTree),
			}
			cur.SubTrees[name] = tree
			tree.Parent = cur
		}
		cur = tree
	}
	return cur
}

func (p *program) getPackage(path string, pro *ast.ProjectFile) *ast.Package {
	var tree = p.GetPackageTree(path)
	if tree != nil {
		for _, pkg := range tree.Packages {
			if pkg.ProjectFile == pro {
				return pkg
			}
		}
	}
	return nil
}

func (p *program) getResolvedPackage(pkg *ast.Package) *ast.Package {
	if pkg.Status&ast.Loaded == 0  {
		infos, _ := ioutil.ReadDir(pkg.DiskPath.Filename())
		for _, info := range infos {
			if !info.IsDir() {
				// path ext container dot.
				if path.Ext(info.Name()) == ".mi" {
					var filePath = filepath.Join(pkg.DiskPath.Filename(), info.Name())
					var uri = xpath.URIFromPath(filePath)
					pkg.Files[uri.Filename()] = p.findSourceFile(xpath.URIFromPath(filePath))
				}
			}
		}
		pkg.Status |= ast.Loaded
	}

	if pkg.Status&ast.Bind == 0 {
		performance.Mark("beforeBind")
		binder.BindPackage(pkg)
		pkg.Status |= ast.Bind
		performance.Mark("afterBind")
		performance.Measure("Bind", "beforeBind", "afterBind")
	}

	return pkg
}

func (p *program) loadProjectFile(pro *ast.ProjectFile) {
	if containProjectFile(p.loadProjectStack, pro) {
		p.errors = append(p.errors, diagnostic.CreateCompilerDiagnostic(diagnostic.M_Project_file_0_circular_dependency, pro.Uri))
	}
	p.loadProjectStack = append(p.loadProjectStack, pro)

	dirs, err := system.GetDirs(pro.SourcePath)
	if err != nil {
		p.errors = append(p.errors, diagnostic.CreateCompilerDiagnostic(diagnostic.M_Read_dir_0_error_colon_1, pro.SourcePath, err.Error()))
		return
	}
	for _, dir := range dirs {
		p.findPackage(filepath.Clean(dir), pro)
	}

	for _, dependent := range pro.Dependents {
		pro.ResolvedDependents = append(pro.ResolvedDependents, p.processProjectFile(xpath.URIFromPath(dependent), false))
	}
	p.loadProjectStack = p.loadProjectStack[:len(p.loadProjectStack)-1]
}

func (p *program) GetDiagnosticsProducingTypeChecker() checker.TypeChecker {
	if p.diagnosticsProducingTypeChecker == nil {
		p.diagnosticsProducingTypeChecker = checker.CreateTypeChecker(p, true)
	}
	return p.diagnosticsProducingTypeChecker
}

func (p *program) DropDiagnosticsProducingTypeChecker() {
	p.diagnosticsProducingTypeChecker = nil
}

func (p *program) GetTypeChecker() checker.TypeChecker {
	if p.noDiagnosticsTypeChecker == nil {
		p.noDiagnosticsTypeChecker = checker.CreateTypeChecker(p, false)
	}
	return p.noDiagnosticsTypeChecker
}

func (p *program) GetEmitResolver() checker.EmitResolver {
	return p.GetDiagnosticsProducingTypeChecker().GetEmitResolver()
}

func (p *program) Emit() emitter.EmitResult {
	if len(p.targetProjects) != 1 {
		panic("The number of target projects is not 1 ")
	}

	return emitter.Emit(p, p.GetProjectFile(p.targetProjects[0]))
}

func (p *program) Build(run bool) builder.BuildResult {
	if len(p.targetProjects) != 1 {
		panic("The number of target projects is not 1 ")
	}

	return builder.Build(p.GetProjectFile(p.targetProjects[0]), run)
}

func (p *program) findPackage(diskPath string, project *ast.ProjectFile) *ast.Package {
	var packagePath = diskPath[len(project.SourcePath):]
	packagePath = strings.Trim(packagePath, string(filepath.Separator))
	packagePath = strings.ReplaceAll(packagePath, string(filepath.Separator), ".")

	if pkg := p.getPackage(packagePath, project); pkg != nil {
		return pkg
	}

	var uri = xpath.URIFromPath(diskPath)
	var pkg = p.host.GetPackage(uri, project)
	project.Packages[diskPath] = pkg

	var tree = p.makePackagePath(packagePath)
	tree.Packages = append(tree.Packages, pkg)
	pkg.Tree = tree

	return pkg
}

func (p *program) findSourceFile(uri xpath.URI) *ast.SourceFile {
	return p.host.GetSourceFile(uri, ast.Java8, func(hostErrorMessage string) {
		p.errors = append(p.errors, diagnostic.CreateCompilerDiagnostic(diagnostic.M_Cannot_read_file_0_Colon_1, uri.Filename(), hostErrorMessage))
	})
}

func (p *program) GetNodeCount() int {
	if p.notFoundMIROOT {
		return 0
	}
	return p.GetDiagnosticsProducingTypeChecker().GetNodeCount()
}

func (p *program) GetIdentifierCount() int {
	if p.notFoundMIROOT {
		return 0
	}
	return p.GetDiagnosticsProducingTypeChecker().GetIdentifierCount()
}

func (p *program) GetSymbolCount() int {
	if p.notFoundMIROOT {
		return 0
	}
	return p.GetDiagnosticsProducingTypeChecker().GetSymbolCount()
}

func (p *program) GetTypeCount() int {
	if p.notFoundMIROOT {
		return 0
	}
	return p.GetDiagnosticsProducingTypeChecker().GetTypeCount()
}

